﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace uTILLIty.ChartBuilder.Core.Extensions
{
	public static class ObjectExtensions
	{

		/// <summary>
		/// Retrieves the schema saved as an assembly-ressource
		/// </summary>
		/// <typeparam name="T">The namespace and assembly of the supplied type to look for the schema-resource</typeparam>
		/// <param name="resourceKey">The name of the resource within the namespace and assembly of the provided <typeparamref name="T">type</typeparamref></param>
		/// <returns>A new instance of the schema loaded from the assembies resources</returns>
		public static XmlSchema GetSchemaResource<T>(string resourceKey)
		{
			using (Stream data = typeof(T).Assembly.GetManifestResourceStream(typeof(T).Namespace + "." + resourceKey))
			{
				if (data == null)
					throw new NotSupportedException(String.Format("Cannot locate resource '{0}'", resourceKey));

				var schema = XmlSchema.Read(data, null);
				return schema;
			}
		}

		public static void XmlSerialize(this object target, Stream outputStream)
		{
			var serializer = new XmlSerializer(target.GetType());
			serializer.Serialize(outputStream, target);
		}

		/// <summary>
		/// Creates a new object by deserializing the <paramref name="configData"/> supplied
		/// </summary>
		/// <param name="configData">a stream representing the XML file to deserialize</param>
		/// <param name="schemas">an optional list of schemas to use for validating the <paramref name="configData"/></param>
		/// <returns>A new instance of <typeparam name="T"></typeparam></returns>
		public static T Load<T>(Stream configData, XmlSchemaSet schemas) where T : class
		{
			if (configData == null)
				throw new ArgumentNullException("configData");

			var settings = new XmlReaderSettings { ValidationType = ValidationType.None };
			if (schemas != null)
			{
				settings.ValidationEventHandler += OnValidationEvent;
				settings.ValidationType = ValidationType.Schema;
				settings.Schemas.Add(schemas);
			}

			var serializer = new XmlSerializer(typeof(T));
			using (var reader = XmlReader.Create(configData, settings))
			{
				var output = (T)serializer.Deserialize(reader);
				return output;
			}
		}

		private static void OnValidationEvent(object sender, ValidationEventArgs e)
		{
			switch (e.Severity)
			{
				case XmlSeverityType.Error:
					throw new XmlSchemaValidationException(e.Message, e.Exception);
			}
		}

		public static void Find<T>(this object source, bool exact, Action<T> handler) where T : class
		{
			var itemsVisited = new HashSet<object>();
			var itemsToProcess = new Queue<QueueItem>(50);
			Enqueue(new QueueItem(source, "", ""), itemsToProcess);

			while (itemsToProcess.Count > 0)
			{

				var currentItem = itemsToProcess.Dequeue();
				var currentValue = currentItem.Value;
				if (currentValue == null)
					continue;

				if (itemsVisited.Contains(currentValue))
					continue;
				itemsVisited.Add(currentValue);

				if (IsMatch<T>(currentValue.GetType(), exact))
					handler((T)currentValue);

				foreach (var prop in currentValue.GetType().GetProperties())
				{
					if (prop.PropertyType.IsValueType || prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
						continue; //ignore simple types

					if (prop.GetIndexParameters().Length > 0)
						continue; //not supported!

					if (prop.PropertyType.IsArray)
					{
						int a = 0;
						while (true)
						{
							try
							{
								var arrayItem = prop.GetValue(currentValue, new object[] { a });
								Enqueue(new QueueItem(arrayItem, currentItem.ToString(), prop.Name, a), itemsToProcess);
								a++;
							}
							catch
							{
								break;
							}
						}
					}
					else
					{
						var propValue = prop.GetValue(currentValue, null);

						if (propValue is IEnumerable)
						{
							int a = 0;
							foreach (var arrayItem in (IEnumerable)propValue)
							{
								Enqueue(new QueueItem(arrayItem, currentItem.ToString(), prop.Name, a), itemsToProcess);
								a++;
							}
						}
						else
							Enqueue(new QueueItem(propValue, currentItem.ToString(), prop.Name), itemsToProcess);
					}
				}
			}
		}

		private static void Enqueue(QueueItem item, Queue<QueueItem> queue)
		{
			if (item != null)
				queue.Enqueue(item);
		}

		private static bool IsMatch<T>(Type itemType, bool exact)
		{
			return exact ? itemType == typeof(T) : itemType.IsAssignableFrom(typeof(T));
		}

		private class QueueItem
		{
			public QueueItem(object item, string root, string property)
			{
				Value = item;
				Root = root;
				Property = property;
			}
			public QueueItem(object item, string root, string property, int idx) : this(item, root, property)
			{
				Property = string.Format("{0}[{1}]", property, idx);
			}

			public object Value { get; set; }
			public string Root { get; set; }
			public string Property { get; set; }

			public override string ToString()
			{
				return string.Format("{0}.{1}", Root, Property);
			}
		}
	}
}